package helpers

import (
	"bytes"
	"context"
	"embed"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"strings"
	"text/template"
	"time"

	. "github.com/onsi/gomega"

	"github.com/f5devcentral/f5-bigip-rest-go/utils"
	"github.com/fatih/color"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/util/retry"

	appv1 "k8s.io/api/apps/v1"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type K8SHelper struct {
	Context       context.Context
	kubeconfig    string
	dynamicClient *dynamic.DynamicClient
	kubeClient    *kubernetes.Clientset
	apiResources  []*metav1.APIResourceList
}

type Configs []*unstructured.Unstructured

func NewK8SHelper(ctx context.Context, kubeconfig string) (*K8SHelper, error) {
	slog := utils.LogFromContext(ctx)
	dc, err := newDynamicClient(kubeconfig)
	if err != nil {
		return nil, err
	}
	client, err := newKubeClient(kubeconfig)
	if err != nil {
		return nil, err
	}
	kc := &K8SHelper{
		Context:       ctx,
		kubeconfig:    kubeconfig,
		kubeClient:    client,
		dynamicClient: dc,
	}
	// if kc.clientset == nil.. newDynamicClient panic if fails
	_, rs, err := client.DiscoveryClient.ServerGroupsAndResources()
	if err != nil {
		slog.Warnf("failed to list api resources: %s", err)
		return nil, err
	} else {
		kc.apiResources = rs
	}
	return kc, nil
}

func (k *K8SHelper) Version() string {
	vers, err := k.kubeClient.ServerVersion()
	if err != nil {
		panic(err)
	}
	return vers.String()
}

func (k *K8SHelper) Apply(configs Configs) error {
	slog := utils.LogFromContext(k.Context)

	for _, confyaml := range configs {
		if confyaml.Object == nil {
			continue
		}
		apiver := confyaml.GetAPIVersion()
		ns := confyaml.GetNamespace()
		keyname := utils.Keyname(ns, confyaml.GetName())
		gvk := schema.FromAPIVersionAndKind(apiver, confyaml.GetKind())
		gvr := k.gvk2gvr(gvk)
		// kubectl apply xx resources in prior is not allowed for:
		// {
		// 	Type: "FieldManagerConflict",
		// 	Message: "conflict with \"kubectl-client-side-apply\" using gateway.networking.k8s.io/v1beta1",
		// 	Field: ".spec.addresses",
		// }

		var orig *unstructured.Unstructured
		var err error
		nsScoped := k.namespaceScoped(gvk)
		if !nsScoped {
			orig, err = k.dynamicClient.Resource(gvr).Get(k.Context, confyaml.GetName(), metav1.GetOptions{})
		} else {
			orig, err = k.dynamicClient.Resource(gvr).Namespace(ns).Get(k.Context, confyaml.GetName(), metav1.GetOptions{})
		}

		if err != nil {
			if !strings.Contains(err.Error(), "not found") {
				slog.Warnf("failed to get original resource: %s: %s", ns+"/"+confyaml.GetName(), err.Error())
				return err
			} else {
				applyOps := metav1.ApplyOptions{FieldManager: utils.Keyname(gvr.Group, gvr.Version)}

				var err error
				if nsScoped {
					_, err = k.dynamicClient.Resource(gvr).Namespace(ns).Apply(k.Context, confyaml.GetName(), confyaml, applyOps)
				} else {
					_, err = k.dynamicClient.Resource(gvr).Apply(k.Context, confyaml.GetName(), confyaml, applyOps)
				}

				if err == nil {
					slog.Infof("==> applied %s/%s %s", color.YellowString(gvk.GroupVersion().String()), color.YellowString(gvk.Kind), color.RedString(keyname))
				} else {
					slog.Warnf("failed to apply %s/%s %s: %s", gvk.GroupVersion().String(), gvk.Kind, keyname, err.Error())
					return err
				}
			}
		} else {
			newa := orig.DeepCopy()
			for k, v := range confyaml.Object {
				newa.Object[k] = v
			}
			var err error
			if nsScoped {
				_, err = k.dynamicClient.Resource(gvr).Namespace(ns).Update(k.Context, newa, metav1.UpdateOptions{})
			} else {
				_, err = k.dynamicClient.Resource(gvr).Update(k.Context, newa, metav1.UpdateOptions{})
			}

			if err != nil {
				applyOps := metav1.ApplyOptions{FieldManager: utils.Keyname(gvr.Group, gvr.Version)}
				if nsScoped {
					_, err = k.dynamicClient.Resource(gvr).Namespace(ns).Apply(k.Context, confyaml.GetName(), confyaml, applyOps)
				} else {
					_, err = k.dynamicClient.Resource(gvr).Apply(k.Context, confyaml.GetName(), confyaml, applyOps)
				}

				if err == nil {
					slog.Infof("==> applied %s/%s %s", color.YellowString(gvk.GroupVersion().String()), color.YellowString(gvk.Kind), color.RedString(keyname))
				} else {
					slog.Warnf("failed to apply %s/%s %s: %s", gvk.GroupVersion().String(), gvk.Kind, keyname, err.Error())
					return err
				}
			} else {
				slog.Infof("==> updated %s/%s %s", color.YellowString(gvk.GroupVersion().String()), color.YellowString(gvk.Kind), color.RedString(keyname))
			}
		}
	}

	return nil
}

func (k *K8SHelper) Delete(configs Configs) error {
	slog := utils.LogFromContext(k.Context)
	errs := []error{}
	for i := len(configs) - 1; i >= 0; i-- {
		confyaml := configs[i]
		if confyaml.Object == nil {
			continue
		}
		apiver := confyaml.GetAPIVersion()
		ns := confyaml.GetNamespace()
		keyname := utils.Keyname(ns, confyaml.GetName())
		gvk := schema.FromAPIVersionAndKind(apiver, confyaml.GetKind())
		gvr := k.gvk2gvr(gvk)

		err := k.dynamicClient.Resource(gvr).Namespace(ns).Delete(k.Context, confyaml.GetName(), metav1.DeleteOptions{})
		if err == nil {
			slog.Infof("==> deleted %s/%s %s", color.YellowString(gvk.GroupVersion().String()), color.YellowString(gvk.Kind), color.RedString(keyname))
		} else {
			if strings.Contains(err.Error(), "not found") {
				slog.Warnf("already removed: %s/%s %s", gvk.GroupVersion().String(), gvk.Kind, keyname)
				continue
			} else {
				slog.Warnf("failed to delete %s/%s %s: %s", gvk.GroupVersion().String(), gvk.Kind, keyname, err.Error())
				errs = append(errs, err)
			}
		}
	}
	return utils.MergeErrors(errs)
}

func (k *K8SHelper) DeleteYaml(yaml string) error {
	return k.Delete(*k.MustLoads(yaml))
}

func (k *K8SHelper) ApplyYaml(yaml string) error {
	return k.Apply(*k.MustLoads(yaml))
}

func (k *K8SHelper) MustLoads(yaml string) *Configs {
	if configs, err := k.Loads(yaml); err != nil {
		panic(err)
	} else {
		return configs
	}
}

func (k *K8SHelper) MustLoad(fs io.Reader) *Configs {
	if configs, err := k.Load(fs); err != nil {
		panic(err)
	} else {
		return configs
	}
}

func (k *K8SHelper) Loads(yaml string) (*Configs, error) {
	configs := []*unstructured.Unstructured{}
	slog := utils.LogFromContext(k.Context)

	// resources := strings.Split(yaml, "---")
	resources := SplitYaml(yaml)
	for _, res := range resources {
		trimed := res
		trimed = strings.Trim(trimed, " ")
		trimed = strings.Trim(trimed, "\n")
		trimed = strings.Trim(trimed, "\t")
		if trimed == "" {
			continue
		}
		jd, err := Yaml2Json([]byte(res))
		if err != nil {
			slog.Warnf("failed to convert yaml content to json: %s: %s", res, err.Error())
			return nil, err
		}
		var j map[string]interface{}
		if err := json.Unmarshal(jd, &j); err != nil {
			slog.Warnf("failed to unmarshal %s: %s", jd, err.Error())
			return nil, err
		}
		confyaml := unstructured.Unstructured{
			Object: j,
		}
		if confyaml.GetNamespace() == "" && k.namespaceScoped(confyaml.GroupVersionKind()) {
			confyaml.SetNamespace("default")
		}
		configs = append(configs, &confyaml)
	}

	if len(configs) == 0 {
		slog.Warnf("found %d resources", len(configs))
	}
	return (*Configs)(&configs), nil
}

func (k *K8SHelper) Load(fs io.Reader) (*Configs, error) {
	slog := utils.LogFromContext(k.Context)

	if b, err := io.ReadAll(fs); err != nil {
		slog.Warnf("failed to read: %s", err.Error())
		return nil, err
	} else {
		return k.Loads(string(b))
	}
}

func (k *K8SHelper) LoadAndRender(fs io.Reader, data map[string]interface{}) (*Configs, error) {
	slog := utils.LogFromContext(k.Context)

	if b, err := io.ReadAll(fs); err != nil {
		slog.Warnf("failed to read: %s", err.Error())
		return nil, err
	} else if s, err := k.render(string(b), data); err != nil {
		slog.Warnf("failed to render: %s", err.Error())
		return nil, err
	} else {
		// slog.Infof(s)
		return k.Loads(s)
	}
}

func (k *K8SHelper) render(j2yaml string, data map[string]interface{}) (string, error) {
	slog := utils.LogFromContext(k.Context)
	tmpl, err := template.New("").Parse(j2yaml)
	if err != nil {
		return "", err
	}
	var buff bytes.Buffer
	if err := tmpl.Execute(&buff, data); err != nil {
		return "", err
	}

	slog.Debugf("rendered yaml: %s", base64.StdEncoding.EncodeToString(buff.Bytes()))
	return buff.String(), nil
}

func (k *K8SHelper) MustRenderToString(j2yaml string, data map[string]interface{}) string {
	if rlt, err := k.render(j2yaml, data); err != nil {
		panic(err)
	} else {
		return rlt
	}
}

func (k *K8SHelper) MustLoadAndRenderYaml(embeds embed.FS, path string, yamlData map[string]interface{}) *Configs {
	f, err := embeds.Open(path)
	Expect(err).To(Succeed())
	defer f.Close()
	y, err := k.LoadAndRender(f, yamlData)
	Expect(err).To(Succeed())

	return y
}

func (k *K8SHelper) gvk2gvr(gvk schema.GroupVersionKind) schema.GroupVersionResource {
	for _, rs := range k.apiResources {
		if gvk.GroupVersion().String() != rs.GroupVersion {
			continue
		}
		for _, r := range rs.APIResources {
			if r.Kind == gvk.Kind {
				return schema.GroupVersionResource{
					Group:    gvk.Group,
					Version:  gvk.Version,
					Resource: r.Name,
				}
			}
		}
		break
	}
	// should not happen
	p, _ := meta.UnsafeGuessKindToResource(gvk)
	return p
}

func (k *K8SHelper) namespaceScoped(gvk schema.GroupVersionKind) bool {
	for _, rs := range k.apiResources {
		if gvk.GroupVersion().String() != rs.GroupVersion {
			continue
		}
		for _, r := range rs.APIResources {
			if r.Kind == gvk.Kind {
				return r.Namespaced
			}
		}
		break
	}

	// should not happen
	return false
}

func (k *K8SHelper) CNIType() string {
	nodes, err := k.kubeClient.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
	if err != nil {
		panic(err)
	}
	if len(nodes.Items) <= 0 {
		panic(fmt.Errorf("no valid node found"))
	}
	for _, node := range nodes.Items {
		for _, c := range node.Status.Conditions {
			if c.Reason == "CiliumIsUp" {
				return "cilium"
			}
			if c.Reason == "CalicoIsUp" {
				return "calico"
			}
			if c.Reason == "FlannelIsUp" {
				return "flannel"
			}
		}
	}
	return "unknown"
}

func (k *K8SHelper) GetEndpoints(namespace, name string) *v1.Endpoints {
	slog := utils.LogFromContext(k.Context)
	eps, err := k.kubeClient.CoreV1().Endpoints(namespace).Get(k.Context, name, metav1.GetOptions{})
	if err != nil {
		slog.Warnf("failed to get endpoints %s/%s: %s", namespace, name, err.Error())
		return nil
	} else {
		return eps
	}
}

func (k *K8SHelper) GetConfigMap(namespace, name string) *v1.ConfigMap {
	slog := utils.LogFromContext(k.Context)
	cm, err := k.kubeClient.CoreV1().ConfigMaps(namespace).Get(k.Context, name, metav1.GetOptions{})
	if err != nil {
		slog.Warnf("failed to get configmap %s/%s", namespace, name)
		return nil
	} else {
		return cm
	}
}

func (k *K8SHelper) GetConfigMapDataWithKey(namespace, name string, key string) []byte {
	slog := utils.LogFromContext(k.Context)
	cm, err := k.kubeClient.CoreV1().ConfigMaps(namespace).Get(k.Context, name, metav1.GetOptions{})
	if err != nil {
		slog.Warnf("failed to get configmap %s/%s", namespace, name)
		return nil
	} else {
		return []byte(cm.Data[key])
	}
}

func (k *K8SHelper) GetNamespace(name string) *v1.Namespace {
	slog := utils.LogFromContext(k.Context)
	ns, err := k.kubeClient.CoreV1().Namespaces().Get(k.Context, name, metav1.GetOptions{})
	if err != nil {
		slog.Warnf("failed to get namespace %s: %s", name, err.Error())
		return nil
	} else {
		return ns
	}
}

func (k *K8SHelper) GetEndpointsAddresses(namespace, name string) []string {
	slog := utils.LogFromContext(k.Context)
	eps := k.GetEndpoints(namespace, name)
	if eps == nil {
		slog.Warnf("not found the eps")
		return []string{}
	} else {
		rlt := []string{}
		for _, subset := range eps.Subsets {
			for _, addrs := range subset.Addresses {
				rlt = append(rlt, addrs.IP)
			}
		}
		return utils.Unified(rlt)
	}
}

func (k *K8SHelper) ScaleDeployment(namespace, name string, to int32) error {
	slog := utils.LogFromContext(k.Context)
	slog.Infof("==> scaled %s %s/%s to %d", color.YellowString("Deployment"), color.RedString(namespace), color.RedString(name), to)
	retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
		// Retrieve the latest version of Deployment before attempting update
		// RetryOnConflict uses exponential backoff to avoid exhausting the apiserver
		result, getErr := k.kubeClient.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{})
		if getErr != nil {
			return fmt.Errorf("failed to get latest version of Deployment: %v", getErr)
		}

		depl := result.DeepCopy()
		depl.Spec.Replicas = &to
		_, updateErr := k.kubeClient.AppsV1().Deployments(namespace).Update(context.TODO(), depl, metav1.UpdateOptions{})
		return updateErr
	})
	if retryErr != nil {
		return fmt.Errorf("update failed: %v", retryErr)
	}
	return nil
}

func (k *K8SHelper) GetDeployment(namespace, name string) *appv1.Deployment {
	slog := utils.LogFromContext(k.Context)
	result, getErr := k.kubeClient.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{})
	if getErr != nil {
		slog.Warnf("failed to get deployment: %s: %s", utils.Keyname(namespace, name), getErr.Error())
		return nil
	} else {
		return result
	}
}

func (k *K8SHelper) DeleteService(namespace, name string) error {
	slog := utils.LogFromContext(k.Context)
	slog.Infof("==> deleted %s %s/%s", color.YellowString("Service"), color.RedString(namespace), color.RedString(name))
	return k.kubeClient.CoreV1().Services(namespace).Delete(k.Context, name, metav1.DeleteOptions{})
}

func (k *K8SHelper) DeleteConfigmap(namespace, name string) error {
	slog := utils.LogFromContext(k.Context)
	slog.Infof("==> deleted %s %s/%s", color.YellowString("Configmap"), color.RedString(namespace), color.RedString(name))
	return k.kubeClient.CoreV1().ConfigMaps(namespace).Delete(k.Context, name, metav1.DeleteOptions{})
}

func (k *K8SHelper) DeleteDeployment(namespace, name string) error {
	slog := utils.LogFromContext(k.Context)
	slog.Infof("==> deleted %s %s/%s", color.YellowString("Deployment"), color.RedString(namespace), color.RedString(name))
	return k.kubeClient.AppsV1().Deployments(namespace).Delete(k.Context, name, metav1.DeleteOptions{})
}

func (k *K8SHelper) GetNodeIPs() []string {
	nl, err := k.kubeClient.CoreV1().Nodes().List(k.Context, metav1.ListOptions{})
	if err != nil {
		panic(err)
	}
	rlt := []string{}
	for _, n := range nl.Items {
		for _, ip := range n.Status.Addresses {
			if ip.Type == v1.NodeInternalIP {
				rlt = append(rlt, ip.Address)
			}
		}
	}
	return utils.Unified(rlt)
}

func (k *K8SHelper) GetSecret(namespace, name string) *v1.Secret {
	slog := utils.LogFromContext(k.Context)
	secret, err := k.kubeClient.CoreV1().Secrets(namespace).Get(k.Context, name, metav1.GetOptions{})
	if err != nil {
		slog.Warnf("failed to get secret %s/%s: %s", namespace, name, err.Error())
		return nil
	} else {
		return secret
	}
}

func (k *K8SHelper) IsAppDeployedWithImageArgs(deployment string, contrainerName string, image string, args []string) bool {
	nsn := strings.Split(deployment, "/")
	depl := k.GetDeployment(nsn[0], nsn[1])
	if depl != nil {
		if depl.Status.AvailableReplicas == 0 {
			return false
		}
		for _, container := range depl.Spec.Template.Spec.Containers {
			if container.Name != contrainerName {
				continue
			}
			c, d, _ := utils.Diff(container.Args, args)
			if len(c)+len(d) == 0 && container.Image == image {
				return true
			}
		}
	}
	return false
}

func (k *K8SHelper) IsDeploymentUpdatedAfter(deployment string, afterTime time.Time) bool {
	nsn := strings.Split(deployment, "/")
	depl := k.GetDeployment(nsn[0], nsn[1])
	if depl != nil {
		if depl.Status.AvailableReplicas == 0 {
			return false
		}
		for _, cond := range depl.Status.Conditions {
			if cond.LastUpdateTime.After(afterTime) {
				return true
			}
		}
	}
	return false
}
